์›ํ‹ฐ๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ 3-1 ๊ณผ์ œํšŒ๊ณ 

@choi2021 ยท November 11, 2022 ยท 20 min read

๐Ÿ“œ ๊ณผ์ œ ์„ค๋ช…

์ด๋ฒˆ ๊ณผ์ œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€๋กœ ๋˜์–ด์žˆ๊ณ  ๊ฒ€์ƒ‰์ฐฝ๊ณผ ๊ฒ€์ƒ‰์–ด ์ถ”์ฒœ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ œ์˜€๋‹ค. ๊ฒ€์ƒ‰์ฐฝ์„ UI๋กœ ๊ตฌํ˜„ํ•œ ํ›„์— ๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅ์— ๋”ฐ๋ผ ๊ด€๋ จ๋œ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ api๋ฅผ ์ด์šฉํ•ด ๋ฐ›์•„์™€ ๋ณด์—ฌ์ฃผ๋ฉด ๋˜๋Š” ๊ฐ„๋‹จํ•ด ๋ณด์˜€๋˜... ํ•˜์ง€๋งŒ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ณต๋ถ€ํ•  ๊ฒŒ ๋งŽ์•˜๋˜ ์œ ์ตํ•œ ๊ณผ์ œ์˜€๋‹ค.

์„ธ๋ถ€ ๋ถ€๋ถ„์€ ์ด 4๊ฐ€์ง€๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  1. ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด์—์„œ ํ‚ค์›Œ๋“œ ๋ณผ๋“œ์ฒ˜๋ฆฌ
  2. APIํ˜ธ์ถœํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋กœ์ปฌ ์บ์‹ฑ
  3. ์ž…๋ ฅ๋งˆ๋‹ค apiํ˜ธ์ถœ์„ ํ•˜์ง€ ์•Š๊ฒŒ api ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ค„์ด๊ธฐ
  4. ํ‚ค๋ณด๋“œ ๋งŒ์œผ๋กœ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋กœ ์ด๋™ํ•˜๊ฒŒ ํ•˜๊ธฐ

์ €๋ฒˆ๊ณผ ๋‹ฌ๋ฆฌ React Query๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ, ์ด์œ ๋Š” React Query๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ ์บ์‹ฑํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ œํ•œ๋˜์—ˆ๊ณ , Typescript๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ง„ํ–‰ํ–ˆ๋‹ค.

๐ŸŽˆ ์ „์—ญ์ƒํƒœ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ

ํŠน๋ณ„ํžˆ ์ด๋ฒˆ๊ณผ์ œ๋ฅผ ํ•˜๋ฉด์„œ ์‹ ๊ฒฝ์ผ๋˜ ๋ถ€๋ถ„์€ ์ „์—ญ์ƒํƒœ์˜€๋‹ค. ๋‹น์—ฐํžˆ ๊ณ ๋ฏผํ•ด์•ผํ•  ๋ถ€๋ถ„์ด์ง€๋งŒ ์ €๋ฒˆ ๊ณผ์ œ๋ฅผ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉด์„œ ์ „์—ญ์ƒํƒœ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š”์ง€ ๋””ํ…Œ์ผํ•˜๊ฒŒ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ์ €๋ฒˆ ๊ณผ์ œ์—์„œ ๋งˆ์ง€๋ง‰์— react query๋ฅผ ์ด์šฉํ•ด ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋ฉด์„œ ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์€ ํ›„์— ๋‹ค์‹œ context API๋ฅผ ์ด์šฉํ•ด ์ „์—ญ์ƒํƒœ๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค. ๋ฉ˜ํ† ๋‹˜๊ป˜์„œ Server State๋ฅผ ๋‹ค์‹œ Client State๋กœ ๋‹ค๋ฃฐ ์ด์œ ๊ฐ€ ์—†๋‹ค๊ณ  ๋ง์”€ํ•ด์ฃผ์…จ๋‹ค.

Server State์™€ Client State, ์ฒ˜์Œ ์ƒ๊ฐํ•ด๋ณธ ์ฃผ์ œ์˜€๋‹ค. ๋‹น์—ฐํžˆ server์—์„œ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด client์˜ ์ „์—ญ์ƒํƒœ๋กœ ๋ฐ›์•„์„œ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋‹ค. server State์™€ client State๋Š” ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ๊ฑธ๊นŒ?

Server state์™€ Client state

๋‘˜์˜ ์ฐจ์ด์— ๋Œ€ํ•ด์„œ ์ฐพ์•„๋ณด๋ฉด์„œ React Query์™€ ์ƒํƒœ๊ด€๋ฆฌ :: 2์›” ์šฐ์•„ํ•œํ…Œํฌ์„ธ๋ฏธ๋‚˜ ๊ธ€์„ ์ฝ์œผ๋ฉด์„œ ์ดํ•ดํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

Client State

  • client์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ƒํƒœ
  • ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ๊ณต์œ ํ•˜์ง€ ์•Š๊ณ  client ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž์™€ interaction์œผ๋กœ ๋ณ€ํ™”๊ฐ€๋Šฅ
  • client ์—์„œ ์ตœ์‹ ์œผ๋กœ ๊ด€๋ฆฌ๋จ

Server State

  • server์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ƒํƒœ
  • fetch์™€ ๊ฐ™์ด ๋„คํŠธ์›Œํฌ api๊ฐ€ ํ•„์š”ํ•จ
  • ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ๊ณต์œ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋กœ ๋ณ€๊ฒฝ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ ์ˆ˜ ์—†์„ ์ˆ˜ ์žˆ์Œ

๋‘˜์˜ ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ฃผ์ฒด๊ฐ€ ๋ˆ„๊ตฌ๋ƒ์— ์žˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ด์ „ ๋ฐฉ์‹์ฒ˜๋Ÿผ api๋ฅผ ํ˜ธ์ถœํ•œ ํ›„์— ๊ฒฐ๊ณผ๋ฅผ ๋กœ์ปฌ์˜ ์ „์—ญ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค๋ฉด ์‚ฌ์‹ค์€ ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ ๋ฐ˜์˜ํ•˜๊ธฐ ์–ด๋ ค์šด ๋ฌธ์ œ๊ฐ€ ์กด์žฌํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฐ ๋ถ€๋ถ„์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค ๋งŒ์˜ ๋ฐฉ๋ฒ•์ด ์žˆ์ง€๋งŒ react Query๋ฅผ ์ด์šฉํ•˜๋ฉด stale time์™€ ๊ฐ™์€ option๋“ค์„ ์ด์šฉํ•ด ์‹œ๊ฐ„์„ ์ •ํ•ด ์ƒˆ๋กœ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๊ณ , ์ž๋™ ๋กœ์ปฌ cahche๊ธฐ๋Šฅ๋„ ์ œ๊ณตํ•˜๊ธฐ์— Server state์™€ Client state ๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ํฐ ์žฅ์ ์„ ๊ฐ–๊ฒŒ๋œ๋‹ค. ์ด์ œ์•ผ react query์˜ ํ•„์š”์„ฑ์„ ์ œ๋Œ€๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๊ณ , ๋‹ค์Œ step์œผ๋กœ ๊ทธ๋Ÿผ ์–ด๋–ค๊ฑธ client์˜ ์ „์—ญ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•ด์•ผํ• ๊นŒ?

์–ด๋–ค ๊ธฐ์ค€์œผ๋กœ ์ „์—ญ์ƒํƒœ๋ฅผ ์„ ํƒํ• ๊นŒ?

Server state์™€ Client state ๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ณ  ๋‚˜๋‹ˆ ๋‚ด๊ฐ€ ์ƒ๊ฐํ•ด์™”๋˜ ์ „์—ญ ์ƒํƒœ์˜ ๊ธฐ์ค€์ด ํ”๋“ค๋ ธ๋‹ค. ์–ด๋–ค ๊ธฐ์ค€์œผ๋กœ ์ „์—ญ์ƒํƒœ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ ์ง€๊ฐ€ ๋‹ค์Œ ๊ณ ๋ฏผ์œผ๋กœ ์ด์–ด์กŒ๋‹ค. ํ•ด๋‹ต์€ context API๋ฅผ ์ด์šฉํ•˜๋Š” ์ด์œ ์™€ ๋™์ผํ–ˆ๋‹ค, Prop-drilling์„ ๋ง‰๊ธฐ ์œ„ํ•ด. ๋ฐ˜๋ณต์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๋Š” ๊ฒฝ์šฐ ๋‚ด ๊ธฐ์ค€์€ 2๋ฒˆ ์ด์ƒ์˜ component๋ฅผ ๊ฑฐ์ณ์„œ ์ „๋‹ฌํ•ด์ค˜์•ผํ•œ๋‹ค๋ฉด ํ•˜๋‚˜์˜ client ์ „์—ญ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ์ข‹๊ฒ ๋‹ค๋Š” ๊ธฐ์ค€์ด ์ƒ๊ฒผ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ „์—ญ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์€ contextAPI, redux, recoil๊ณผ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋‹ค์ผ๊นŒ?

Query String: ์žŠํ˜€์ง„ ์ „์—ญ์ƒํƒœ

์ƒํƒœ๊ด€๋ฆฌ๋ผ ํ•˜๋ฉด contextAPI๋‚˜ redux์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ์„ ๋– ์˜ฌ๋ ธ์ง€๋งŒ, ์ด๋ฒˆ ์ˆ˜์—…์‹œ๊ฐ„์— ํ•˜๋‚˜์˜ ์ „์—ญ์ƒํƒœ ๊ด€๋ฆฌ๋ฐฉ๋ฒ•์œผ๋กœ Query String์— ๋Œ€ํ•ด ๋ฉ˜ํ† ๋‹˜๊ป˜์„œ ์•Œ๋ ค์ฃผ์…จ๋‹ค. ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์š”์†Œ์ด์ง€๋งŒ ๊ณ ๋ฏผํ•˜์ง€ ๋ชปํ–ˆ๋˜ ๋ถ€๋ถ„์ด์—ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์‡ผํ•‘๋ชฐ ์‚ฌ์ดํŠธ์—์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ํ•„ํ„ฐ(ํ‚ค, ์˜ท ์ข…๋ฅ˜ ๋“ฑ)๋ฅผ ์ ์šฉํ•œ ํ›„์— ์นœ๊ตฌ์—๊ฒŒ ๊ณต์œ ํ•œ๋‹ค๊ณ  ํ–ˆ์„ ๋•Œ ํ™”๋ฉด๊ณผ url์ด ๋™๊ธฐํ™” ๋˜์–ด์žˆ์ง€ ์•Š๋‹ค๋ฉด ์นœ๊ตฌ๊ฐ€ ๋ณด๋Š” ํ™”๋ฉด์€ ๋‚ด๊ฐ€ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์–ดํ–ˆ๋˜ ํŽ˜์ด์ง€์™€๋Š” ๋‹ค๋ฅธ ํŽ˜์ด์ง€ ๋  ๊ฒƒ์ด๋‹ค.

์ด๋ ‡๊ฒŒ query String์„ ํ•˜๋‚˜์˜ ์ „์—ญ์ƒํƒœ๋กœ ์ƒ๊ฐํ•ด query String์— ๋”ฐ๋ผ ํ™”๋ฉด์„ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๊ณ , ์ด๋Ÿฌํ•œ ๋ฐฉ๋ฒ•์€ ์„ธ๋ถ€์ ์ธ ํ•„ํ„ฐ๋‚˜, ์ž…๋ ฅ๊ฐ’์„ ๊ด€๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

(์‹ค์ œ ๊ตฌ๊ธ€ ๊ฒ€์ƒ‰์ฐฝ์— ๋‚ด์šฉ์„ ์ž…๋ ฅํ•œ ํ›„์— ๊ตฌ๊ธ€์˜ url์„ ๋ณด๋ฉด ๋ฐ˜์˜๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค)

  1

์ด๋ฒˆ ๊ณผ์ œ๋ฅผ ํ•˜๋ฉด์„œ ์›๋ž˜๋Š” keyword๋ผ๋Š” ์ „์—ญ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ณ  input์˜ ๊ฐ’์ด ๋ณ€ํ™”ํ•  ๋•Œ๋งˆ๋‹ค keyword์ƒํƒœ๋ฅผ ๋ณ€ํ™”์‹œ์ผœ api๋ฅผ ํ˜ธ์ถœํ•˜๋ ค ํ–ˆ์ง€๋งŒ, query String์„ ๋ฐ”๊ฟˆ์œผ๋กœ์จ query์— ๋”ฐ๋ผ api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ํ•ด๋ณด๋ฉด ์–ด๋–จ๊นŒ๋ผ๋Š” ์ƒ๊ฐ์œผ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.

์ฒ˜์Œ์— useParams๋ฅผ ์ด์šฉํ•˜๋ฉด query String์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์„๊นŒ ํ–ˆ์ง€๋งŒ useParms๋กœ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ๋ง๊ทธ๋Œ€๋กœ paramter, /:id๋ผ๋ฉด id๊ฐ’๋งŒ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์—ˆ๋‹ค. query string์„ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด์„œ๋Š” window.location์„ ์ด์šฉํ•˜๊ฑฐ๋‚˜ react-router-dom์˜ useLocation์„ ์ด์šฉํ•ด์„œ path๋ฅผ ๋ฐ›๊ณ  ๋ณ„๋„์˜ qs ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•ด object๋กœ ๋งŒ๋“  ํ›„์— ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ถ”๊ฐ€์ ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—†์ด, ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์–ด useSearchParam์ด๋ผ๋Š” react-router-dom์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

useSearchParam์€ useState์ฒ˜๋Ÿผ ๋ฐฐ์—ด์˜ ์ฒซ ์š”์†Œ๋Š” ํ˜„์žฌ url์˜ ํŒŒ๋žŒ์ด ๋‹ด๊ฒจ์žˆ๊ณ , ๋‘๋ฒˆ์งธ ์š”์†Œ๋Š” param์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” setState์™€ ๊ฐ™์€ ํ•จ์ˆ˜๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค. ๊ฐ’๋งŒ ๋ฐ›์•„์˜ค๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ฒซ์š”์†Œ์˜ ๋ฉ”์†Œ๋“œ์ธ get์œผ๋กœ ํ•ด๋‹น query string์„ ๋ฐ›์•„ ํ•˜๋‚˜์˜ ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ–ˆ๋‹ค.

import { useSearchParams } from "react-router-dom"

const useQueryString = () => {
  const [params] = useSearchParams()
  const query = params.get("q") || ""
  return query
}

export { useQueryString }

๐Ÿ’พ ๋กœ์ปฌ ์บ์‹ฑ

์ด๋ฒˆ ๊ณผ์ œ์˜ ํ•ต์‹ฌ์ ์ธ ๋ถ€๋ถ„์ค‘ ํ•˜๋‚˜์˜€๋˜ ๋กœ์ปฌ ์บ์‹ฑ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ HTTP cache-control, local Storage, Session Storage ์„ธ๊ฐ€์ง€์— ๋Œ€ํ•ด์„œ ๊ณ ๋ คํ–ˆ๊ณ , Session Storage๋ฅผ ์ด์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

HTTP cache-control

HTTP cache-control์€ apiํ˜ธ์ถœ ํ›„์— ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ ํ›„์— ์ดํ›„ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ–ˆ์„ ๋•Œ ์ƒˆ๋กœ api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, cache๋˜์–ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ, ์„œ๋ฒ„ state์˜ ๋ณ€ํ™”๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋ผ ์ƒ๊ฐ๋˜์—ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์ƒ์—์„œ๋Š” cache data์˜ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ ํฌ๊ธฐ๊ฐ€ ์ž‘๊ณ , ์„œ๋ฒ„์˜ ์‘๋‹ต์ด 200๊ณผ 304๋กœ ๊ตฌ๋ถ„๋˜์–ด ํ‘œ์‹œ๋˜์—ˆ์ง€๋งŒ, client์—์„œ์˜ ์„œ๋ฒ„ ์‘๋‹ต์ด ๋ชจ๋‘ ๋™์ผํ•˜๊ฒŒ 200 OK๋กœ ๋“ค์–ด์™€ cache๋œ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ค์šด ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.

[์„œ๋ฒ„์˜ ์‘๋‹ต]

[client๊ฐ€ ๋ฐ›์€ ์„œ๋ฒ„์˜ ์‘๋‹ต]

Browser Storage

๋‹ค์Œ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ๋‚ด์˜ storage๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ–ˆ๋‹ค. ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ธŒ๋ผ์šฐ์ €๋กœ ๊ฐ€์ ธ์™€์„œ client์—์„œ ์ €์žฅํ•˜๋‹ค๋ณด๋‹ˆ ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™”๋ฅผ ์•Œ ์ˆ˜ ์—†๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋ธŒ๋ผ์šฐ์ €์— ๋‹ซ์•„๋„ ๊ณ„์†ํ•ด์„œ ์ €์žฅํ•˜๋Š” local storage๊ฐ€ ์•„๋‹ˆ๋ผ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ์œผ๋ฉด ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ , ์ƒˆ๋กœ์šด api ํ˜ธ์ถœ์„ ํ•˜๋Š” session storage๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒŒ ๋” ์ ์ ˆํ•œ ์„ ํƒ์ด๋ผ ์ƒ๊ฐ๋˜์—ˆ๋‹ค.

session storage๋ฅผ ์ฒ˜์Œ ์จ๋ดค์ง€๋งŒ local Stoarge์™€ ๋™์ผํ•œ method๋ผ ์‚ฌ์šฉ๋ฒ•์€ ๊ฐ„๋‹จํ–ˆ๊ณ , ์ €์žฅํ•  ๋•Œ๋Š” JSON.Stringfy๋ฅผ ๋ฐ›์•„์˜ฌ ๋•Œ๋Š” JSON.parse๋ฅผ ์ด์šฉํ•ด ๋ณ€ํ™”ํ•ด์ค˜์•ผํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— CacheService class๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ–ˆ๋‹ค.

export default class CacheService {
  static setData = (query: string, words: SearchType[]) => {
    const stringifyWord = JSON.stringify(words)
    sessionStorage.setItem(query, stringifyWord)
  }

  static getData = (query: string) => {
    const data = sessionStorage.getItem(query)
    const parsedData: SearchType[] = JSON.parse(data || JSON.stringify([]))
    return parsedData
  }
}

๋‹ค๋ฅธ ํŒ€์˜ ์ฝ”๋“œ์—์„œ map์„ ์ด์šฉํ•ด ๋กœ์ปฌ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•œ ๋ถ€๋ถ„์„ ๋ณด๋ฉด์„œ ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜๋„ ์žˆ์—ˆ๊ฒ ๊ตฌ๋‚˜ ๋А๋ผ๊ธฐ๋„ ํ–ˆ๋‹ค.

const myCache = new Map()

export const setMyCacheData = <T>(key: string, data: T) => {
  const value = { data, expired: new Date().getTime() + 5000 }
  myCache.set(key, value)
}

export const getMyCacheData = (key: string) => {
  if (myCache.has(key)) {
    if (myCache.get(key).expired > new Date().getTime()) {
      return myCache.get(key).data
    } else {
      myCache.delete(key)
    }
  }

  return null
}

# ๐ŸŽจํ‚ค์›Œ๋“œ Bold ์ฒ˜๋ฆฌ

ํ‚ค์›Œ๋“œ๋Š” queryString์„ ์ด์šฉํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ api ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น query๋ฅผ bold์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ณ„๋„์˜ ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•ด์•ผํ–ˆ๋‹ค. string์„ tag๋กœ ๋ฐ”๊ฟ”์ค˜์•ผํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ด€๋ จ ๋‚ด์šฉ์„ ์ฐพ์•„๋ณด์•˜๋”๋‹ˆ ๊ฐ€์žฅ ๋จผ์ € ๋‚˜์˜จ๊ฑด dangerouslySetInnerHTML์ด์—ˆ๋‹ค. ์ด๋ฆ„๋ถ€ํ„ฐ ์“ฐ์ง€๋ง๋ผ๋Š” ๊ฒƒ ๊ฐ™์•„ ์™œ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋Š”์ง€ ๋จผ์ € ์ฐพ์•„๋ณด๋‹ˆ, ๋ฌธ์ž์—ด์„ ํƒœ๊ทธ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž„์˜๋กœ script๋ฅผ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ณด์•ˆ๋ฌธ์ œ๊ฐ€ ์กด์žฌํ–ˆ๋‹ค.

์ด๋ฅผ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด ์šฐ์„  ๋ฌธ์ž์—ด ๋‚ด์˜ query๋ถ€๋ถ„์„ ๊ธฐ์ค€์œผ๋กœ split์œผ๋กœ array๋ฅผ ๋งŒ๋“ค์–ด ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๋Š” index์—๋งŒ <b></b>๋กœ ๊ฐ์‹ธ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค. ํ‚ค์›Œ๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ํ•ญ์ƒ ํ‚ค์›Œ๋“œ๋Š” ๋‘๋ฒˆ์จฐ์— ์กด์žฌํ•ด ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

//splitByKeyword.ts
const splitByKeyword = (query: string, text: string) => {
  if (text.toUpperCase().includes(query.trim().toUpperCase())) {
    return text.split(new RegExp(`(${query})`, 'gi'));
  }
};

export { splitByKeyword };

//searchItem.tsx

const KEYWORD_INDEX = 1;

const SearchItem = ({
 ...
}: SearchItemProps) => {

  const query = useQueryString();
  const textArray = splitByKeyword(query, text);

  return (
    <Link to={`/search?q=${text}`}>
      <S.Wrapper ref={itemRef} active={active} onMouseEnter={handleMouseEnter}>
        <BsSearch />
        <span>
          {textArray?.map((item, idx) => {
            if (idx === KEYWORD_INDEX) {
              return <b key={item}>{item}</b>;
            }
            return item;
          })}
        </span>
      </S.Wrapper>
    </Link>
  );
};

export default SearchItem;

โœจDebouncing๊ณผ Throttling

์ž…๋ ฅ๋งˆ๋‹ค APIํ˜ธ์ถœํ•˜์ง€ ์•Š๋„๋ก ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋‹ค๊ฐ€ Debouncing๊ณผ Throttling์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ์‚ฌ์‹ค ์ด์ „ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ œ์—์„œ scroll event๋ฅผ ์ด์šฉํ•ด ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ Debouncing๊ณผ Throttling์žˆ๋‹ค๋Š” ์ ์„ ๋ณธ ์ ์ด ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋•Œ๋Š” Intersection Observer API๋ฅผ ์ด์šฉํ–ˆ์–ด์„œ ํฌ๊ฒŒ ๊ณต๋ถ€๋ฅผ ์•ˆํ–ˆ์ง€๋งŒ ์ด๋ฒˆ๊ธฐํšŒ์— ์ œ๋Œ€๋กœ ๊ณต๋ถ€ํ•ด๋ณด๊ณ ์ž ํ–ˆ๋‹ค.

Debouncing

Debouncing์€ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์›ํ•˜๋Š” ์‹œ์ ์˜ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ด๋ฒˆ๊ณผ์ œ์—์„œ๋Š” input์˜ onChange๋กœ ์ธํ•ด ์ƒ๊ธฐ๋Š” ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ api๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฐ›์•„์™€์•ผํ–ˆ๋‹ค. ๋งŒ์•ฝ์— ๋ชจ๋“  event์— ๋Œ€ํ•ด์„œ api๋ฅผ ๋ถˆ๋Ÿฌ์˜จ๋‹ค๋ฉด ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ์ด ์ƒ๊ธฐ๊ธฐ ๋•Œ๋ฌธ์— onChange์˜ event ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ์„ ๋งˆ์ณค์„ ๋•Œ์˜ input value๋ฅผ api๋กœ ํ˜ธ์ถœํ•˜๋Š”๊ฒŒ ๋” ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์ด ๋˜์—ˆ๋‹ค. ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž…๋ ฅ๋œ ๊ฐ’์„ api๋กœ ์ „๋‹ฌํ•˜๋Š” debounce๋ฐฉ์‹์„ ์ด์šฉํ–ˆ๋‹ค.

useEffect(() => {
  if (query) {
    if (NO_SESSION_ITEM) {
      dispatch({ type: "SET_DATA", data: cachedItem })
    } else {
      const debounce = setTimeout(() => {
        getResponse()
      }, DELAY_TIME)
      return () => clearTimeout(debounce)
    }
  }
}, [query])

setTimeout์„ ์ด์šฉํ•ด delay time ๋’ค์— ์ „๋‹ฌํ•ด์ค€ callback ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ํ•˜๋Š”๋ฐ, ์ด๋•Œ input์˜ ์ž…๋ ฅ์œผ๋กœ query๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ด์ „ ์‹คํ–‰๋Œ€๊ธฐ์ค‘์ด๋˜ debounce ํ•จ์ˆ˜๋Š” clearTimemout์œผ๋กœ ์ธํ•ด ์‚ฌ๋ผ์ง€๊ฒŒ ๋˜๊ณ  ๋งˆ์ง€๋ง‰์œผ๋กœ ์ „๋‹ฌํ•ด์ค€ ๊ฐ’๋งŒ ์‹คํ–‰์‹œํ‚ค๋Š” ๋ฐฉ์‹์œผ๋กœ debouncing์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

Throttling

Throttling์€ ์ผ์ • ์‹œ๊ฐ„์„ ๋‘๊ณ  ์‹œ๊ฐ„ ์•ˆ์˜ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋งŒ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. debouncing๊ณผ ๋‹ค๋ฅธ ์ ์œผ๋กœ ์ •ํ•ด์ง„ ์‹œ๊ฐ„๋‚ด์— ๋‹ค๋ฅธ ์ž…๋ ฅ์ด ๋“ค์–ด์™€๋„ ๋ฌด์‹œํ•˜๊ณ  ์ •ํ•ด์ง„ ์‹œ๊ฐ„์ด ์ง€๋‚œ ์ดํ›„์— ๋‹ค์‹œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์‹คํ–‰์‹œํ‚จ๋‹ค๋Š” ์ ์ด๋‹ค. ๋‘˜์„ ์ •๋ฆฌํ•˜๋ฉด ์ฃผ๊ธฐ์ ์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์ด Throttling, ์ฒ˜์Œ์ด๋‚˜ ๋์— ๋“ค์–ด์˜จ ์ด๋ฒคํŠธ๋งŒ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด Debouncing์œผ๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

debouncing์€ ๊ฒ€์ƒ‰๊ณผ ๊ฐ™์ด ์‹œ์ ์˜ ๊ฒฐ๊ณผ๊ฐ€ ์ค‘์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๊ณ , throttling์€ ๋ฌดํ•œ ์Šคํฌ๋กค๊ณผ ๊ฐ™์€ ๊ณผ์ œ์—์„œ ์„ฑ๋Šฅ๊ฐœ์„ ์„ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฒˆ๊ธฐํšŒ์— ์ œ๋Œ€๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋˜์—ˆ๋‹ค.


โŒจ ํ‚ค๋ณด๋“œ๋งŒ์œผ๋กœ ์ถ”์ฒœ๊ฒ€์ƒ‰์–ด๋กœ ์ด๋™๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„

ํ‚ค๋ณด๋“œ๋งŒ์œผ๋กœ ์ถ”์ฒœ๊ฒ€์ƒ‰์–ด๋“ค๋กœ ์ด๋™ํ•˜๊ธฐ ์œ„ํ•ด, ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ ๋ฐฐ์—ด์˜ index์™€ keydown, keyup event๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

window์˜ keydown, keyup Event๋ฅผ ์‚ฌ์šฉํ•ด ํ™”๋ฉด์—์„œ ๋ฐฉํ–ฅํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ๋ฐ”๋กœ ์ถ”์ฒœ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ๋กœ ์ด๋™์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด, ๋ณ€ํ™”๋œ index์™€ searchItem์˜ index๊ฐ€ ๊ฐ™์„๋•Œ ํ•ด๋‹น searchItem ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ๊ฒฝ์ƒ‰์„ ๋ณ€ํ™”์‹œ์ผœ ํ˜„์žฌ ์œ„์น˜๋ฅผ UI๋กœ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.

์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ ๋ฐฐ์—ด์˜ ์ž๋ฃŒ ์–‘์ด ๋งŽ์•„์ง€๋ฉด scroll์ด ์ƒ๊ธฐ๊ณ  container ํฌ๊ธฐ ๋ฐ–์˜ item์ด ๋ณด์ด์ง€ ์•Š๋Š” ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ scrollIntoView ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ํ•ด๋‹น ์•„์ดํ…œ์˜ ์œ„์น˜๋กœ ์ž๋™์œผ๋กœ ์Šคํฌ๋กค์ด ๋  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.

๋งˆ์šฐ์Šค์ด๋™๊ณผ ํ‚ค๋ณด๋“œ์ด๋™์ด ํ˜ธํ™˜์ด ๊ฐ€๋Šฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ mouse Event์— ๋”ฐ๋ผ์„œ๋„ index๊ฐ€ ๋ณ€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋Š”๋ฐ, ์ฒ˜์Œ์— mouseMove ์ด๋ฒคํŠธ๋ฅผ ์ด์šฉํ–ˆ๋”๋‹ˆ ๋„ˆ๋ฌด ๋งŽ์€ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•ด ๋‹นํ™ฉํ•˜๊ธฐ๋„ ํ–ˆ๋‹ค. ๋งˆ์šฐ์Šค๊ฐ€ ํ•ด๋‹น item์— ์˜ฌ๋ผ์™”์„ ๋•Œ๋งŒ index๋ฅผ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋˜๋‹ˆ๊นŒ mouseEnter๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ ์ ˆํ•œ ์ด๋ฒคํŠธ ์„ ํƒ์ด ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฒƒ๋„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ์˜€๋‹ค.

//useKeyboard
import { useState, useEffect } from 'react';

type KeyType = 'ArrowDown' | 'ArrowUp' | 'Enter';

const useKeyPress = (targetKey: KeyType) => {

const [keyPressed, setKeyPressed] = useState(false);

const downHandler = (event: KeyboardEvent) => {
  const { key } = event;
  if (key === targetKey) {
    setKeyPressed(true);
    }
  };

const upHandler = (event: KeyboardEvent) => {
  const { key } = event;
  if (key === targetKey) {
    setKeyPressed(false);
    }
  };

useEffect(() => {
  window.addEventListener('keydown', downHandler);
  window.addEventListener('keyup', upHandler);

  return () => {
    window.removeEventListener('keydown', downHandler);
    window.removeEventListener('keyup', upHandler);
   };
 });

return keyPressed;
};

export { useKeyPress };

//SearchItem.tsx
const KEYWORD_INDEX = 1;

const SearchItem = ({
 active
 ...
}: SearchItemProps) => {
  const itemRef = useRef<HTMLLIElement>(null);

  const handleMouseEnter = () => {
    setIsMovingMouse(true);
    setCursor(index);
  };
  useEffect(() => {
    if (active && !isMovingMouse) {
      itemRef.current?.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
      });
    }
  });
  return (
    <Link to={`/search?q=${text}`}>
      <S.Wrapper ref={itemRef} active={active} onMouseEnter={handleMouseEnter}>
        <BsSearch />
        <span>
          {textArray?.map((item, idx) => {
            if (idx === KEYWORD_INDEX) {
              return <b key={item}>{item}</b>;
            }
            return item;
          })}
        </span>
      </S.Wrapper>
    </Link>
  );
};

export default SearchItem;

๐Ÿ˜€ ๋งˆ์น˜๋ฉฐ

์ด์ œ ๋งˆ์ง€๋ง‰ ๊ณผ์ œ๋งŒ์„ ์•ž์— ๋‘๊ณ  ์žˆ๋‹ค. ๋‹ค์Œ์ฃผ๋ฉด ํ•œ๋‹ฌ๊ฐ„์˜ ๊ณผ์ •์ด ๋๋‚˜๊ณ  ์ง„์งœ ์ง€์›์„ ํ•˜๊ฒŒ๋˜๋Š”๋ฐ ๊ฑฑ์ •๋˜๊ณ  ๋‘๋ ต๊ธฐ๋„ ํ•˜๋‹ค. ํ•˜์ง€๋งŒ ๋ฐฐ์šด ๋‚ด์šฉ์„ ํ†ตํ•ด ๋„“ํ˜€์ง„ ์‹œ์•ผ์™€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด์„œ ๋ฐฐ์šด ๋” ์ข‹์€ ์ฝ”๋“œ๋“ค์„ ๋‚ด๊ฐ€ ํ•ด์™”๋˜ ํ”„๋กœ์ ํŠธ๋“ค์— ์ ์šฉํ•ด ๊ฐ„๋‹ค๋ฉด ๋ถ„๋ช… ๋” ํฐ ๊ฒฝ์Ÿ๋ ฅ๊ณผ ํž˜์ด ๋  ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ž์‹ ๊ฐ๋„ ์ƒ๊ธด๋‹ค. ๊ณต๋ถ€ํ•  ๊ฑด ๋งŽ์ง€๋งŒ, ์กฐ๊ธ‰ํ•œ ๋งˆ์Œ๋ณด๋‹ค ๊ธฐ์œ ๋งˆ์Œ์œผ๋กœ ๋ฐฐ์›Œ๊ฐ€๋Š” ํƒœ๋„๋ฅผ ์œ ์ง€ํ•ด๊ฐ€์ž.

@choi2021
๋งค์ผ์˜ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๊ฐœ๋ฐœ์ผ์ง€์ž…๋‹ˆ๋‹ค.